//
// Created by martin on 4/2/22.
//

#include "FtpDataClient.h"
#include "FtpResponse.h"
#include "muduo/base/Logging.h"
#include "muduo/net/Connector.h"
#include "muduo/net/InetAddress.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/SocketsOps.h"


namespace ftp
{
namespace detail
{

void removeConnection(muduo::net::EventLoop* loop, const muduo::net::TcpConnectionPtr& conn)
{
	loop->queueInLoop(std::bind(&muduo::net::TcpConnection::connectDestroyed, conn));
}

void removeConnector(const ConnectorPtr& connector)
{

}

} // namespace detail
} // namespace ftp

using namespace ftp;
using namespace muduo;
using namespace muduo::net;

muduo::AtomicInt64 FtpDataClient::nextConnId_;

FtpDataClient::FtpDataClient(EventLoop* loop, const InetAddress& serverAddr, const string& nameArg)
	: loop_(CHECK_NOTNULL(loop)),
	connector_(new Connector(loop, serverAddr)),
	name_(nameArg),
	connectionCallback_(defaultConnectionCallback),
	messageCallback_(defaultMessageCallback),
	retry_(false),
	connect_(true)
{
	connector_->setNewConnectionCallback(
		std::bind(&FtpDataClient::newConnection, this, _1));

	LOG_DEBUG << "FtpDataClient::FtpDataClient[" << name_
		<< "] - connector " << get_pointer(connector_);
}

FtpDataClient::~FtpDataClient()
{
	LOG_DEBUG << "FtpDataClient::~FtpDataClient[" << name_
		<< "] - connector " << get_pointer(connector_);

	TcpConnectionPtr conn;
	bool unique = false;
	{
		unique = connection_.unique(); // true: use_count == 1
		conn = connection_;
	}

	// remove TcpConnection conn if not null
	if (get_pointer(conn)) { // not null
		assert(loop_ == conn->getLoop());
		CloseCallback cb = std::bind(&ftp::detail::removeConnection, loop_, _1);
		loop_->runInLoop(
			std::bind(&TcpConnection::setCloseCallback, conn, cb));

		if (unique) {
			conn->forceClose(); // force close connection if there is only one holds connection
		}
	}
	else { // null
		connector_->stop();
		loop_->runAfter(1, std::bind(&ftp::detail::removeConnector, connector_));
	}
}

/**
 * @brief Create a new TcpConnection object used for ftp data connection,
 * and set attributes for the object.
 *
 * It will be called back by EventLoop when receive a Tcp connection request (Three-Handshake).
 */
void FtpDataClient::newConnection(int sockfd)
{
	loop_->assertInLoopThread();

	InetAddress peerAddr(sockets::getPeerAddr(sockfd));
	char buf[32];
	snprintf(buf, sizeof(buf), ":%s#%ld", peerAddr.toIpPort().c_str(), nextConnId_.get());
	nextConnId_.increment();
	std::string connName = name_ + buf;

	InetAddress localAddr(sockets::getLocalAddr(sockfd));
	TcpConnectionPtr conn(new TcpConnection(loop_,
		connName,
		sockfd,
		localAddr,
		peerAddr));
	// Deliver event callback to TcpConnection
	conn->setConnectionCallback(std::bind(&FtpDataClient::onConnection, this, _1));
	conn->setMessageCallback(std::bind(&FtpDataClient::onMessage, this, _1, _2, _3));
	conn->setWriteCompleteCallback(std::bind(&FtpDataClient::onWriteComplete, this, _1));
	conn->setCloseCallback(std::bind(&FtpDataClient::removeConnection, this, _1));

	// this object owns the TcpConnection object
	connection_ = conn;

	// ! Very important to ensure TcpConnection has been established 
	// after creating TcpConnection object 
	conn->connectEstablished();
}

void FtpDataClient::removeConnection(const muduo::net::TcpConnectionPtr& conn)
{
	loop_->assertInLoopThread();
	assert(loop_ == conn->getLoop());

	{
		assert(connection_ == conn);
		connection_.reset();
	}

	loop_->queueInLoop(std::bind(&TcpConnection::connectDestroyed, conn));
	if (retry_ && connect_) {
		LOG_INFO << "TcpClient::connect[" << name_ << "] - Reconnecting to "
			<< connector_->serverAddress().toIpPort();
		connector_->restart();
	}
}

void FtpDataClient::onConnection(const TcpConnectionPtr& conn)
{
	loop_->assertInLoopThread();

	if (connectionCallback_) {
		connectionCallback_(conn);
	}
}

void FtpDataClient::onMessage(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp receiveTime)
{
	if (messageCallback_) {
		messageCallback_(conn, buffer, receiveTime);
	}
}

void FtpDataClient::onWriteComplete(const TcpConnectionPtr& conn)
{
	if (writeCompleteCallback_) {
		writeCompleteCallback_(conn);
	}
}

/**
 * @brief Try to connect to Ftp Data Server (Ftp Client).
 *
 * Set callbacks before connect(), so that FtpDataClient knows how to handle
 * when a new connection request comes.
 */
void FtpDataClient::connect()
{
	loop_->assertInLoopThread();

	LOG_INFO << "FtpDataClient::connect[" << name_ << "] - connecting to "
		<< connector_->serverAddress().toIpPort();
	connect_ = true;
	connector_->start();
}

/**
 * @brief Try to disconnect the data connection with Ftp Data Server.
 *
 * i.e. shutdown wirte
 */
void FtpDataClient::disconnect()
{
	loop_->assertInLoopThread();

	LOG_INFO << "FtpDataClient::disconnect[" << name_ << "] - disconnecting to "
		<< connector_->serverAddress().toIpPort();
	connect_ = false;

	{
		if (get_pointer(connection_)) {
			connection_->shutdown();
		}
	}
}

void FtpDataClient::stop()
{
	loop_->assertInLoopThread();

	connect_ = false;
	connector_->stop();
}
